package org.durandj.ystream.server.network;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.channels.SocketChannel;
import java.security.InvalidParameterException;

import org.durandj.ystream.server.Server;
import org.durandj.ystream.server.message.Message;
import org.durandj.ystream.server.util.ByteBufferUtils;
import org.durandj.ystream.server.util.InetAddressUtils;
import org.durandj.ystream.server.util.Timer;

/**
 * A single client from the client list. This class
 * is more of a utility class for keeping all client
 * related information organized. Each client is
 * also given a watchdog timer that makes sure that if
 * a client does not keep in communication with the
 * server it gets dropped accordingly.
 * 
 * @author durandj
 * @version 1.0
 *
 */
public class Client
{
	/**
	 * The watchdog timer time delay.
	 */
	private static final long WATCHDOG_TIME = 90000;
	
	/**
	 * The socket used to communicate with the client.
	 */
	private SocketChannel socketChannel;
	
	/**
	 * Handles the sending of packets to the client.
	 */
	private PacketSender packetSender;
	
	/**
	 * A watchdog timer for the client that maintains the connection.
	 * When the watchdog timer runs out the client is disconnected.
	 */
	private Timer watchdog;
	
	/**
	 * A flag that is normally off. When it is set that means the client
	 * is marked for disconnect.
	 */
	private boolean dirty;
	
	/**
	 * The latency of the connection in milliseconds.
	 */
	private long latency;
	
	/**
	 * The counter variable used for averaging ping times.
	 */
	private long avgPingCount;
	
	/**
	 * Creates a client with the specified information.
	 * 
	 * @param clientSocket - the socket connection to the client
	 * 
	 * @throws UnknownHostException when the client connection could not be established
	 * @throws IOException if something occurs when checking the socket
	 */
	public Client(SocketChannel clientSocket) throws UnknownHostException, IOException
	{
		if(clientSocket == null)
			throw new NullPointerException("Must give a valid socket.");
		
		if(!clientSocket.isConnected())
			throw new InvalidParameterException("Must give a connected socket.");
		
		this.socketChannel = clientSocket;
		this.socketChannel.configureBlocking(false);
		this.packetSender = new PacketSender(this.socketChannel);
		this.latency = 0;
		this.watchdog = new Timer(WATCHDOG_TIME,
		                          Server.getServer().getClientManager(),
		                          Message.Priority.MEDIUM,
		                          ByteBufferUtils.stringToByteBuffer(InetAddressUtils.getStringAddress(getClientAddress())));
		this.watchdog.start();
		
		avgPingCount = 0;
	}
	
	@Override
	public void finalize()
	{
		try
		{
			if(this.socketChannel.isOpen())
				this.socketChannel.close();
		}
		catch(IOException e)
		{}
	}
	
	/**
	 * Gets a reference to the client's socket channel.
	 * 
	 * @return a socket channel
	 */
	public SocketChannel getChannel()
	{
		return socketChannel;
	}
	
	/**
	 * Gets the address of the client.
	 * 
	 * @return the client's address
	 */
	public InetAddress getClientAddress()
	{
		return socketChannel.socket().getInetAddress();
	}
	
	/**
	 * Gets a reference to the client's packet sender.
	 * 
	 * @return the packet sender
	 */
	public PacketSender getPacketSender()
	{
		return packetSender;
	}
	
	/**
	 * Feeds the watchdog. This should only happen after
	 * the client has revalidated the connection to the
	 * server.
	 */
	public void feedWatchdog()
	{
		watchdog.stop();
		new Timer(50);
		watchdog.reset(WATCHDOG_TIME);
		watchdog.start();
	}
	
	/**
	 * Stops the watchdog. This should only happen
	 * when the client is being disconnected.
	 */
	public void killWatchdog()
	{
		watchdog.stop();
	}
	
	/**
	 * Checks if the dirty bit is set.
	 * 
	 * @return the dirty bit
	 */
	public boolean isDirty()
	{
		return dirty;
	}
	
	/**
	 * Sets the dirty bit.
	 * 
	 * @param dirty - the new dirty bit value
	 */
	public void setDirty(boolean dirty)
	{
		this.dirty = dirty;
	}
	
	/**
	 * Gets the latency of the connection in milliseconds.
	 * 
	 * @return the latency
	 */
	public long getLatency()
	{
		return latency;
	}
	
	/**
	 * Determines the new latency of the connection by averaging the current latency
	 * with the new latency.
	 * 
	 * @param latency - the latency
	 */
	public void setLatency(long latency)
	{
		this.latency = (this.latency * avgPingCount + latency) / (avgPingCount + 1);
		avgPingCount++;
	}
	
	@Override
	public boolean equals(Object o)
	{
		if(!(o instanceof Client))
			return false;
		
		if(o == this)
			return true;
		
		Client param = (Client)o;
		return this.socketChannel.equals(param.socketChannel);
	}
	
	@Override
	public String toString()
	{
		return InetAddressUtils.getStringAddress(socketChannel.socket().getInetAddress());
	}
}
